/*
 * Copyright 2012 Gregor Schauer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package at.schauer.gregor.vienna.util;

import at.schauer.gregor.vienna.context.ViennaContext;
import at.schauer.gregor.vienna.convert.provider.ConverterProvider;
import at.schauer.gregor.vienna.datastore.DataStore;
import at.schauer.gregor.vienna.exception.ViennaException;
import at.schauer.gregor.vienna.metadata.MetadataProvider;
import at.schauer.gregor.vienna.serializer.Serializer;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;

import static at.schauer.gregor.vienna.util.ViennaUtils.ConverterUtils.convert;
import static at.schauer.gregor.vienna.util.ViennaUtils.ConverterUtils.hasConverter;

/**
 * @author Gregor Schauer
 */
public final class ViennaUtils {
	private ViennaUtils() {
	}

	public static <X> X filter(Map<Class<?>, X> map, Class clazz) {
		X elem = map.get(clazz);
		if (elem == null) {
			for (Map.Entry<Class<?>, X> entry : map.entrySet()) {
				if (entry.getKey().isAssignableFrom(clazz)) {
					elem = entry.getValue();
					map.put(clazz, elem);
					break;
				}
			}
		}
		return elem;
	}


	public static class TypeUtils {
		private TypeUtils() {
		}

		@Nonnull
		public static Map<Type, Class> getGenericTypes(@Nonnull Class clazz) {
			Map<Type, Class> map = new LinkedHashMap<Type, Class>();
			Type[] genericInterfaces = clazz.getGenericInterfaces();
			for (Type type : genericInterfaces) {
				if (!ParameterizedTypeImpl.class.isAssignableFrom(type.getClass())) {
					type = clazz.getGenericSuperclass();
				}
				Class bar = getGenericTypeArgument(type);
				if (bar != null) {
					map.put(type, bar);
				} else {
					map.putAll(getGenericTypes(clazz.getSuperclass()));
				}
			}
			if (genericInterfaces.length == 0) {
				Type type = clazz.getGenericSuperclass();
				if (type != null) {
					map.put(type, getGenericTypeArgument(type));
				}
			}
			return map;
		}

		@Nullable
		public static Class getGenericTypeArgument(@Nullable Type type) {
			if (type != null && ParameterizedTypeImpl.class.isAssignableFrom(type.getClass())) {
				ParameterizedTypeImpl parameterizedType = ParameterizedTypeImpl.class.cast(type);
				for (Type actualTypeArgument : parameterizedType.getActualTypeArguments()) {
					if (actualTypeArgument.getClass() == Class.class) {
						return Class.class.cast(actualTypeArgument);
					} else if (ParameterizedTypeImpl.class.isAssignableFrom(actualTypeArgument.getClass())) {
						return ParameterizedTypeImpl.class.cast(actualTypeArgument).getRawType();
					}
				}
			}
			return null;
		}
	}


	public static class ConverterUtils {
		private ConverterUtils() {
		}

		@SuppressWarnings("unchecked")
		protected static <T> T convert(ViennaContext ctx, Object obj, Class<T> targetClass) {
			if (obj.getClass() == targetClass) {
				return (T) obj;
			}

			try {
				ConverterProvider<?> converterProvider = ctx.getConverterProvider(obj.getClass());
				return converterProvider.convert(obj, targetClass);
			} catch (Exception e) {
				throw new ViennaException("Cannot convert '" + obj + "' to '" + targetClass + '\'', e);
			}
		}

		public static boolean hasConverter(ViennaContext ctx, Class<?> sourceClass, Class<?> targetClass) {
			try {
				ConverterProvider<?> converterProvider = ctx.getConverterProvider(sourceClass);
				Object converter = converterProvider != null ? converterProvider.getConverter(sourceClass, targetClass) : null;
				if (converter != null) {
					return true;
				}
			} catch (Exception e) {
				// ignore
			}
			return sourceClass == targetClass;
		}

		@SuppressWarnings("unchecked")
		public static String typeName(ViennaContext ctx, Class clazz) {
			if (clazz == null) {
				return ObjectUtils.Null.class.getSimpleName();
			}
			MetadataProvider infoProvider = ctx.getMetadataProvider(clazz);
			return infoProvider != null ? infoProvider.getEntityName(clazz) : clazz.getSimpleName();
		}

		public static String type(Class clazz) {
			return (clazz != null ? clazz : ObjectUtils.Null.class).getName();
		}

		public static String typeFromString(String str) {
			int posSep = str.indexOf(Serializer.CLASS_OBJECT_SEPARATOR);
			int posEq = str.indexOf('=') + 1;
			return posSep > 0 ? posEq > posSep ? str.substring(0, posSep) : str.substring(posEq, posSep) : str;
		}

		public static String valueFromString(String str) {
			int pos = str.indexOf(Serializer.CLASS_OBJECT_SEPARATOR) + Serializer.CLASS_OBJECT_SEPARATOR.length();
			return pos > 0 ? str.substring(pos) : str;
		}
	}


	public static class SerializationUtils {
		private SerializationUtils() {
		}

		@SuppressWarnings("unchecked")
		public static <T> String save(ViennaContext ctx, T obj) {
			if (obj == null) {
				return String.valueOf(obj);
			}

			boolean hasConverter = hasConverter(ctx, obj.getClass(), String.class);
			if (hasConverter) {
				return saveViaConverter(ctx, obj);
			} else {
				DataStore<T> dataStore = ctx.getDataStore((Class<T>) obj.getClass());
				return saveViaDataStore(dataStore, obj);
			}
		}

		public static <T, E extends T> String saveViaDataStore(DataStore<T> dataStore, E obj) {
			return String.valueOf(dataStore.save(obj));
		}

		public static String saveViaConverter(ViennaContext ctx, Object obj) {
			return convert(ctx, obj, String.class);
		}
	}


	public static class DeserializationUtils {
		private DeserializationUtils() {
		}

		@SuppressWarnings("unchecked")
		public static Object load(ViennaContext ctx, Class clazz, String value) {
			if (clazz == null || clazz == ObjectUtils.Null.class) {
				return null;
			}

			boolean hasConverter = hasConverter(ctx, String.class, clazz);
			if (hasConverter) {
				return loadViaConverter(ctx, clazz, value);
			} else {
				return loadViaDataStore(ctx, clazz, value);
			}
		}

		@SuppressWarnings("unchecked")
		public static <T> T loadViaDataStore(ViennaContext ctx, Class<T> clazz, String value) {
			MetadataProvider metadataProvider = ctx.getMetadataProvider(clazz);
			if (metadataProvider != null) {
				String idName = metadataProvider.getIdentifierName(clazz);
				if (idName != null) {
					Field field = ReflectionUtils.findField(clazz, idName);
					Object key = load(ctx, field.getType(), value);
					return ctx.getDataStore(clazz).load(clazz, key);
				}
			}
			return ctx.getDataStore(clazz).load(clazz, value);
		}

		public static <T> T loadViaConverter(ViennaContext ctx, Class<T> clazz, String value) {
			return convert(ctx, value, clazz);
		}
	}
}
